<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>11 真实示例 - 完整拖拽应用 · ShaofeiZi Blog · 做个日常记录</title>
    <meta name="description" content="訾绍飞的博客。万物皆有裂缝处，那是光射进来的地方。">
    <link rel="shortcut icon" href="/BLOG/favicon.ico">
  <link rel="manifest" href="/BLOG/manifest.json">
  <meta name="theme-color" content="#3F51B5">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <link rel="apple-touch-icon" href="/BLOG/icons/192.png">
  <link rel="mask-icon" href="/BLOG/icons/safari-pinned-tab.svg" color="#3eaf7c">
  <meta name="msapplication-TileImage" content="/icons/192.png">
  <meta name="msapplication-TileColor" content="#3F51B5">
    
    <link rel="preload" href="/BLOG/assets/css/42.styles.90045bd1.css" as="style"><link rel="preload" href="/BLOG/assets/js/app.1a725be8.js" as="script"><link rel="preload" href="/BLOG/assets/js/23.e624ba97.js" as="script"><link rel="prefetch" href="/BLOG/assets/js/7.88ba0bb7.js"><link rel="prefetch" href="/BLOG/assets/js/0.d3e592bd.js"><link rel="prefetch" href="/BLOG/assets/js/1.39b9c99c.js"><link rel="prefetch" href="/BLOG/assets/js/2.68dc10c9.js"><link rel="prefetch" href="/BLOG/assets/js/3.dfebdd5e.js"><link rel="prefetch" href="/BLOG/assets/js/4.ea97a821.js"><link rel="prefetch" href="/BLOG/assets/js/5.d8c2ecbf.js"><link rel="prefetch" href="/BLOG/assets/js/6.e51cd79c.js"><link rel="prefetch" href="/BLOG/assets/js/8.d9eebc06.js"><link rel="prefetch" href="/BLOG/assets/js/9.1a541d13.js"><link rel="prefetch" href="/BLOG/assets/js/10.4ec9ca67.js"><link rel="prefetch" href="/BLOG/assets/js/11.02558377.js"><link rel="prefetch" href="/BLOG/assets/js/12.d0e2086f.js"><link rel="prefetch" href="/BLOG/assets/js/13.5af02ddd.js"><link rel="prefetch" href="/BLOG/assets/js/14.5d9fcbf2.js"><link rel="prefetch" href="/BLOG/assets/js/15.ca0178b2.js"><link rel="prefetch" href="/BLOG/assets/js/16.cd99d056.js"><link rel="prefetch" href="/BLOG/assets/js/17.56f11c1d.js"><link rel="prefetch" href="/BLOG/assets/js/18.21837cc7.js"><link rel="prefetch" href="/BLOG/assets/js/19.73335fea.js"><link rel="prefetch" href="/BLOG/assets/js/20.1632ab79.js"><link rel="prefetch" href="/BLOG/assets/js/21.43175244.js"><link rel="prefetch" href="/BLOG/assets/js/22.5b7c0cca.js"><link rel="prefetch" href="/BLOG/assets/js/24.ac5f7b41.js"><link rel="prefetch" href="/BLOG/assets/js/25.6934a11d.js"><link rel="prefetch" href="/BLOG/assets/js/26.407b2583.js"><link rel="prefetch" href="/BLOG/assets/js/27.7449d673.js"><link rel="prefetch" href="/BLOG/assets/js/28.52e25437.js"><link rel="prefetch" href="/BLOG/assets/js/29.fba21f3a.js"><link rel="prefetch" href="/BLOG/assets/js/30.2cd6d3e2.js"><link rel="prefetch" href="/BLOG/assets/js/31.0b0a749f.js"><link rel="prefetch" href="/BLOG/assets/js/32.92134487.js"><link rel="prefetch" href="/BLOG/assets/js/33.ad2b89cc.js"><link rel="prefetch" href="/BLOG/assets/js/34.9b22334e.js"><link rel="prefetch" href="/BLOG/assets/js/35.825f3d75.js"><link rel="prefetch" href="/BLOG/assets/js/36.cc3da84c.js"><link rel="prefetch" href="/BLOG/assets/js/37.8f339f62.js"><link rel="prefetch" href="/BLOG/assets/js/38.5674618f.js"><link rel="prefetch" href="/BLOG/assets/js/39.180f0d85.js"><link rel="prefetch" href="/BLOG/assets/js/40.275f26e3.js"><link rel="prefetch" href="/BLOG/assets/js/41.ce0f5927.js">
    <link rel="stylesheet" href="/BLOG/assets/css/42.styles.90045bd1.css">
  </head>
  <body>
    <div id="app" data-server-rendered="true"><div data-app="true" id="app" class="application theme--light"><div class="application--wrap"><div class="v-progress-linear blog-progress" style="height:3px;display:none;"><div class="v-progress-linear__background accent" style="height:3px;opacity:0.4;width:100%;"></div><div class="v-progress-linear__bar"><!----><div class="v-progress-linear__bar__determinate accent" style="width:0%;"></div></div></div><aside class="v-navigation-drawer v-navigation-drawer--close v-navigation-drawer--fixed v-navigation-drawer--is-mobile" style="height:100%;margin-top:0px;max-height:calc(100% - 0px);transform:translateX(-240px);width:240px;"><div><div class="aside-brand-wrap"><div class="aside-brand"><a href="/BLOG/" class="aside-avatar elevation-2 router-link-active"><img src="/BLOG/face.png" alt="avatar"></a><hgroup class="mt-3 variant-hide"><div class="subheading white--text">訾绍飞</div><a href="mailto:zishaofei221@gmail.com" title="zishaofei221@gmail.com" class="aside-mail primary--text text--lighten-5">zishaofei221@gmail.com</a></hgroup></div></div><hr class="v-divider theme--dark"><div class="v-list nav-list"><div class="secondary--text"><a href="/BLOG/" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-home"></i></div></div><div class="v-list__tile__content">首页</div></a></div><div class="secondary--text"><a href="/BLOG/tags" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-tag"></i></div></div><div class="v-list__tile__content">标签</div></a></div><div class="secondary--text"><a href="https://github.com/ShaofeiZi" target="_blank" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fab fa-github"></i></div></div><div class="v-list__tile__content">Github</div></a></div><div class="secondary--text"><a href="/BLOG/about" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-user-secret"></i></div></div><div class="v-list__tile__content">About</div></a></div></div></div><div class="v-navigation-drawer__border"></div></aside><nav class="blog-toolbar v-toolbar v-toolbar--fixed theme--dark primary" style="margin-top:0px;padding-right:0px;padding-left:0px;transform:translateY(0px);"><div class="v-toolbar__content" style="height:56px;"><button type="button" class="v-btn v-btn--icon"><div class="v-btn__content"><i class="fa fa-bars"></i></div></button><div class="v-toolbar__title">11 真实示例 - 完整拖拽应用</div><div class="spacer"></div><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""><!----></div><div class="v-menu" style="display:none;"><div class="v-menu__activator"><button type="button" class="v-btn v-btn--icon"><div class="v-btn__content"><i class="fa fa-share-alt"></i></div></button></div><div class="v-menu__content" style="max-height:auto;min-width:0px;max-width:auto;top:12px;left:0px;transform-origin:top right;z-index:0;display:none;"><div class="v-list"><div class="secondary--text"><a class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-lg fa-copy"></i></div></div><div class="v-list__tile__title">复制链接</div></a></div></div><input type="text" tabindex="-1" aria-hidden="true" value="" class="fake-hide"></div></div></div></nav><main class="v-content" style="padding-top:56px;padding-right:0px;padding-bottom:0px;padding-left:0px;"><div class="v-content__wrap"><div class="container blog-container grid-list-xl align-center"><div class="layout row wrap"><div class="flex mb-3 xs12"><article class="v-card elevation-16 post-card" style="height:undefined;"><div class="v-card__title"><div class="flex xs12"><h2 class="display-1 mb-3">11 真实示例 - 完整拖拽应用</h2><div class="post-meta"><time datetime="2018-04-29T12:16:03.000Z" class="secondary--text post-time">2018年04月29日</time></div></div></div><div class="v-card__text pt-0 pb-0"><div class="flex xs12"><div class="content custom"><h1 id="_30-天精通-rxjs-11-：-真实示例-完整拖拽应用"><a href="#_30-天精通-rxjs-11-：-真实示例-完整拖拽应用" aria-hidden="true" class="header-anchor">#</a> 30 天精通 RxJS (11)： 真实示例 - 完整拖拽应用</h1><blockquote><p>有次不小心进到了优酷，发现优酷有个不错的功能，能大大的提升用户体验，就让我们一起来实现这个效果吧！</p></blockquote><p>一样建议大家可以直接看影片</p><p>在第 08 篇的时候，我们已经成功做出简易的拖拽效果，今天要来做一个完整的应用，而且是实务上有机会遇到但不好处理的需求，那就是优酷的影片效果！</p><blockquote><p>如果还没有用过优酷的读者可以先前往<a href="http://v.youku.com/v_show/id_XMTQ5NTg2MDk3Ng==.html?&f=26824174&from=y1.2-3.4.2&spm=a2h0j.8191423.item_XMTQ5NTg2MDk3Ng==.A" target="_blank" rel="noopener noreferrer">这里</a>试用。</p></blockquote><p>当我们在优酷看影片时往下滚动画面，影片会变成一个小视窗在右下角，这个视窗还能够拖拽移动位置。这个功能可以让使用者一边看留言同时又能看影片，且不影响其他的资讯显示，真的是很不错的 feature。</p><p><img src="https://res.cloudinary.com/dohtkyi84/image/upload/v1482841073/30days/youku_drag.png" alt="优酷影片拖拽功能"></p><p>就让我们一起来实现这个功能，同时补完拖拽所需要注意的细节吧！</p><h2 id="需求分析"><a href="#需求分析" aria-hidden="true" class="header-anchor">#</a> 需求分析</h2><p>首先我们会有一个影片在最上方，原本是位置是静态(static)的，卷轴滚动到低于影片高度后，影片改为相对于视窗的绝对位置(fixed)，往回滚会再变回原本的状态。当影片为 fixed 时，滑鼠移至影片上方(hover)会有遮罩(masker)与鼠标变化(cursor)，可以拖拽移动(drag)，且移动范围不超过可视区间！</p><p>上面可以拆分成以下几个步骤</p><ul><li>准备 static 样式与 fixed 样式</li><li>HTML 要有一个固定位置的锚点(anchor)</li><li>当滚动超过锚点，则影片变成 fixed</li><li>当往回滚动过锚点上方，则影片变回 static</li><li>影片 fixed 时，要能够拖拽</li><li>拖拽范围限制在当前可视区间</li></ul><p>基本的 HTML 跟 CSS 笔者已经帮大家完成，大家可以直接到下面的链接接着实现：</p><ul><li><a href="https://jsbin.com/pevozex/1/edit?html,css,js,output" target="_blank" rel="noopener noreferrer">JSBin</a></li><li><a href="https://jsfiddle.net/s6323859/ochbtpk5/1/" target="_blank" rel="noopener noreferrer">JSFiddle</a></li></ul><p>先让我们看一下 HTML，首先在 HTML 里有一个 div(#anchor)，这个 div(#anchor) 就是待会要做锚点用的，它内部有一个 div(#video)，则是滚动后要改变成 fixed 的元件。</p><p>CSS 的部分我们只需要知道滚动到下方后，要把 div(#video) 加上 <code>video-fixed</code> 这个 class。</p><p>接着我们就开始实现滚动的效果切换 class 的效果吧！</p><h3 id="第一步，取得会用到的-dom"><a href="#第一步，取得会用到的-dom" aria-hidden="true" class="header-anchor">#</a> 第一步，取得会用到的 DOM</h3><p>因为先做滚动切换 class，所以这里用到的 DOM 只有 #video, #anchor。</p><pre class="language-javascript"><code><span class="token keyword">const</span> video <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'video'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> anchor <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'anchor'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

</code></pre><h3 id="第二步，建立会用到的-observable"><a href="#第二步，建立会用到的-observable" aria-hidden="true" class="header-anchor">#</a> 第二步，建立会用到的 observable</h3><p>这里做滚动效果，所以只需要监听滚动事件。</p><pre class="language-javascript"><code><span class="token keyword">const</span> scroll <span class="token operator">=</span> Rx<span class="token punctuation">.</span>Observable<span class="token punctuation">.</span><span class="token function">fromEvent</span><span class="token punctuation">(</span>document<span class="token punctuation">,</span> <span class="token string">'scroll'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

</code></pre><h3 id="第三步，撰写程式逻辑"><a href="#第三步，撰写程式逻辑" aria-hidden="true" class="header-anchor">#</a> 第三步，撰写程式逻辑</h3><p>这里我们要取得了 scroll 事件的 observable，当滚过 #anchor 最底部时，就改变 #video 的 class。</p><p>首先我们会需要滚动事件发生时，去判断是否<strong>滚过 #anchor 最底部</strong>，所以把原本的滚动事件变成是否滚过最底部的 true or false。</p><pre class="language-javascript"><code>scroll<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> anchor<span class="token punctuation">.</span><span class="token function">getBoundingClientRect</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>bottom <span class="token operator">&lt;</span> <span class="token number">0</span><span class="token punctuation">)</span>

</code></pre><p>这里我们用到了 <code>getBoundingClientRect</code> 这个浏览器原生的 API，他可以取得 DOM 事件的宽高以及上下左右离萤幕可视区间上(左)的距离，如下图</p><p><img src="https://res.cloudinary.com/dohtkyi84/image/upload/v1482844440/30days/getBoundingClientRect.png" alt></p><p>当我们可视范围区间滚过 #anchor 底部时， <code>anchor.getBoundingClientRect().bottom</code> 就会变成负值，此时我们就改变 #video 的 class。</p><pre class="language-javascript"><code>scroll
<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> anchor<span class="token punctuation">.</span><span class="token function">getBoundingClientRect</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>bottom <span class="token operator">&lt;</span> <span class="token number">0</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span>bool <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">if</span><span class="token punctuation">(</span>bool<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        video<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">'video-fixed'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
        video<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token string">'video-fixed'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>

</code></pre><p>到这里我们就已经完成滚动变更样式的效果了！</p><p>全部的 JS 代码，如下</p><pre class="language-javascript"><code><span class="token keyword">const</span> video <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'video'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> anchor <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'anchor'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">const</span> scroll <span class="token operator">=</span> Rx<span class="token punctuation">.</span>Observable<span class="token punctuation">.</span><span class="token function">fromEvent</span><span class="token punctuation">(</span>document<span class="token punctuation">,</span> <span class="token string">'scroll'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

scroll
<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> anchor<span class="token punctuation">.</span><span class="token function">getBoundingClientRect</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>bottom <span class="token operator">&lt;</span> <span class="token number">0</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span>bool <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">if</span><span class="token punctuation">(</span>bool<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        video<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">'video-fixed'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
        video<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token string">'video-fixed'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>

</code></pre><blockquote><p>当然这段还能在用 debounce/throttle 或 requestAnimationFrame 做优化，这个部分我们日后的文章会在提及。</p></blockquote><p>接下来我们就可以接着做<strong>拖拽的行为</strong>了。</p><h3 id="第一步，取得会用到的-dom-2"><a href="#第一步，取得会用到的-dom-2" aria-hidden="true" class="header-anchor">#</a> 第一步，取得会用到的 DOM</h3><p>这里我们会用到的 DOM 跟前面是一样的(#video)，所以不用多做什么。</p><h3 id="第二步，建立会用到的-observable-2"><a href="#第二步，建立会用到的-observable-2" aria-hidden="true" class="header-anchor">#</a> 第二步，建立会用到的 observable</h3><p>这里跟上次一样，我们会用到 mousedown, mouseup, mousemove 三个事件。</p><pre class="language-javascript"><code><span class="token keyword">const</span> mouseDown <span class="token operator">=</span> Rx<span class="token punctuation">.</span>Observable<span class="token punctuation">.</span><span class="token function">fromEvent</span><span class="token punctuation">(</span>video<span class="token punctuation">,</span> <span class="token string">'mousedown'</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> mouseUp <span class="token operator">=</span> Rx<span class="token punctuation">.</span>Observable<span class="token punctuation">.</span><span class="token function">fromEvent</span><span class="token punctuation">(</span>document<span class="token punctuation">,</span> <span class="token string">'mouseup'</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> mouseMove <span class="token operator">=</span> Rx<span class="token punctuation">.</span>Observable<span class="token punctuation">.</span><span class="token function">fromEvent</span><span class="token punctuation">(</span>document<span class="token punctuation">,</span> <span class="token string">'mousemove'</span><span class="token punctuation">)</span>

</code></pre><h3 id="第三步，撰写程式逻辑-2"><a href="#第三步，撰写程式逻辑-2" aria-hidden="true" class="header-anchor">#</a> 第三步，撰写程式逻辑</h3><p>跟上次是差不多的，首先我们会点击 #video 元件，点击(mousedown)后要变成移动事件(mousemove)，而移动事件会在滑鼠放开(mouseup)时结束(takeUntil)</p><pre class="language-javascript"><code>mouseDown
<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> mouseMove<span class="token punctuation">.</span><span class="token function">takeUntil</span><span class="token punctuation">(</span>mouseUp<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">concatAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span>

</code></pre><p>因为把 mouseDown observable 发送出来的<strong>事件</strong>换成了 mouseMove observable，所以变成了 observable(mouseDown) 送出 observable(mouseMove)。因此最后用 concatAll 把后面送出的元素变成 mouse move 的事件。</p><blockquote><p>这段如果不清楚的可以回去看一下 08 篇的讲解</p></blockquote><p>但这里会有一个问题，就是我们的这段拖拽事件其实只能做用到 video-fixed 的时候，所以我们要加上 <code>filter</code></p><pre class="language-javascript"><code>mouseDown
<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> video<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span><span class="token string">'video-fixed'</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> mouseMove<span class="token punctuation">.</span><span class="token function">takeUntil</span><span class="token punctuation">(</span>mouseUp<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">concatAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span>

</code></pre><p>这里我们用 filter 如果当下 #video 没有 <code>video-dragable</code> class 的话，事件就不会送出。</p><p>再来我们就能跟上次一样，把 mousemove 事件变成 { x, y } 的事件，并订阅来改变 #video 元件</p><pre class="language-javascript"><code>mouseDown
    <span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> video<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span><span class="token string">'video-fixed'</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> mouseMove<span class="token punctuation">.</span><span class="token function">takeUntil</span><span class="token punctuation">(</span>mouseUp<span class="token punctuation">)</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">concatAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>m <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
        <span class="token keyword">return</span> <span class="token punctuation">{</span>
            x<span class="token punctuation">:</span> m<span class="token punctuation">.</span>clientX<span class="token punctuation">,</span>
            y<span class="token punctuation">:</span> m<span class="token punctuation">.</span>clientY
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span>pos <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
        video<span class="token punctuation">.</span>style<span class="token punctuation">.</span>top <span class="token operator">=</span> pos<span class="token punctuation">.</span>y <span class="token operator">+</span> <span class="token string">'px'</span><span class="token punctuation">;</span>
        video<span class="token punctuation">.</span>style<span class="token punctuation">.</span>left <span class="token operator">=</span> pos<span class="token punctuation">.</span>x <span class="token operator">+</span> <span class="token string">'px'</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span>

</code></pre><p>到这里我们基本上已经完成了所有功能，其步骤跟 08 篇的方法是一样的，如果不熟悉的人可以回头看一下！</p><p>但这里有两个大问题我们还没有解决</p><ol><li>第一次拉动的时候会闪一下，不像优酷那么顺</li><li>拖拽会跑出当前可视区间，跑上出去后就抓不回来了</li></ol><p>让我们一个一个解决，首先第一个问题是因为我们的拖拽直接给元件滑鼠的位置(clientX, clientY)，而非给滑鼠相对移动的距离！</p><p>所以要解决这个问题很简单，我们只要把点击目标的左上角当作 (0,0)，并以此改变元件的样式，就不会有闪动的问题。</p><p>这个要怎么做呢？ 很简单，我们在昨天讲了一个 operator 叫做 withLatestFrom，我们可以用它来把 mousedown 与 mousemove 两个 Event 的值同时传入 callback。</p><pre class="language-javascript"><code>mouseDown
    <span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> video<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span><span class="token string">'video-fixed'</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> mouseMove<span class="token punctuation">.</span><span class="token function">takeUntil</span><span class="token punctuation">(</span>mouseUp<span class="token punctuation">)</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">concatAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">withLatestFrom</span><span class="token punctuation">(</span>mouseDown<span class="token punctuation">,</span> <span class="token punctuation">(</span>move<span class="token punctuation">,</span> down<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
        <span class="token keyword">return</span> <span class="token punctuation">{</span>
            x<span class="token punctuation">:</span> move<span class="token punctuation">.</span>clientX <span class="token operator">-</span> down<span class="token punctuation">.</span>offsetX<span class="token punctuation">,</span>
            y<span class="token punctuation">:</span> move<span class="token punctuation">.</span>clientY <span class="token operator">-</span> down<span class="token punctuation">.</span>offsetY
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span>pos <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
        video<span class="token punctuation">.</span>style<span class="token punctuation">.</span>top <span class="token operator">=</span> pos<span class="token punctuation">.</span>y <span class="token operator">+</span> <span class="token string">'px'</span><span class="token punctuation">;</span>
        video<span class="token punctuation">.</span>style<span class="token punctuation">.</span>left <span class="token operator">=</span> pos<span class="token punctuation">.</span>x <span class="token operator">+</span> <span class="token string">'px'</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span>

</code></pre><p>当我们能够同时得到 mousemove 跟 mousedown 的事件，接着就只要把 滑鼠相对可视区间的距离(client) 减掉点按下去时 滑鼠相对元件边界的距离(offset) 就行了。这时拖拽就不会先闪动一下囉！</p><blockquote><p>大家只要想一下，其实 client - offset 就是元件相对于可视区间的距离，也就是他一开始没动的位置！</p></blockquote><p><img src="https://res.cloudinary.com/dohtkyi84/image/upload/v1482854816/30days/offset.png" alt="offset&client"></p><p>接着让我们解决第二个问题，拖拽会超出可视范围。这个问题其实只要给最大最小值就行了，因为需求的关系，这里我们的元件是相对可视居间的绝对位置(fixed)，也就是说</p><ul><li>top 最小是 0</li><li>left 最小是 0</li><li>top 最大是<strong>可视高度</strong>扣掉<strong>元件本身高度</strong></li><li>left 最大是<strong>可视宽度</strong>扣掉<strong>元件本身宽度</strong></li></ul><p>这里我们先宣告一个 function 来处理这件事</p><pre class="language-javascript"><code><span class="token keyword">const</span> <span class="token function-variable function">validValue</span> <span class="token operator">=</span> <span class="token punctuation">(</span>value<span class="token punctuation">,</span> max<span class="token punctuation">,</span> min<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> Math<span class="token punctuation">.</span><span class="token function">min</span><span class="token punctuation">(</span>Math<span class="token punctuation">.</span><span class="token function">max</span><span class="token punctuation">(</span>value<span class="token punctuation">,</span> min<span class="token punctuation">)</span><span class="token punctuation">,</span> max<span class="token punctuation">)</span>
<span class="token punctuation">}</span>

</code></pre><p>第一个参数给原本要给的位置值，后面给最大跟最小，如果今天大于最大值我们就取最大值，如果今天小于最小值则取最小值。</p><p>再来我们就可以直接把这个问题解掉了</p><pre class="language-javascript"><code>mouseDown
    <span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> video<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span><span class="token string">'video-fixed'</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>e <span class="token operator">=&gt;</span> mouseMove<span class="token punctuation">.</span><span class="token function">takeUntil</span><span class="token punctuation">(</span>mouseUp<span class="token punctuation">)</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">concatAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">withLatestFrom</span><span class="token punctuation">(</span>mouseDown<span class="token punctuation">,</span> <span class="token punctuation">(</span>move<span class="token punctuation">,</span> down<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
        <span class="token keyword">return</span> <span class="token punctuation">{</span>
            x<span class="token punctuation">:</span> <span class="token function">validValue</span><span class="token punctuation">(</span>move<span class="token punctuation">.</span>clientX <span class="token operator">-</span> down<span class="token punctuation">.</span>offsetX<span class="token punctuation">,</span> window<span class="token punctuation">.</span>innerWidth <span class="token operator">-</span> <span class="token number">320</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
            y<span class="token punctuation">:</span> <span class="token function">validValue</span><span class="token punctuation">(</span>move<span class="token punctuation">.</span>clientY <span class="token operator">-</span> down<span class="token punctuation">.</span>offsetY<span class="token punctuation">,</span> window<span class="token punctuation">.</span>innerHeight <span class="token operator">-</span> <span class="token number">180</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span>pos <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
        video<span class="token punctuation">.</span>style<span class="token punctuation">.</span>top <span class="token operator">=</span> pos<span class="token punctuation">.</span>y <span class="token operator">+</span> <span class="token string">'px'</span><span class="token punctuation">;</span>
        video<span class="token punctuation">.</span>style<span class="token punctuation">.</span>left <span class="token operator">=</span> pos<span class="token punctuation">.</span>x <span class="token operator">+</span> <span class="token string">'px'</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span>

</code></pre><p>这里我偷懒了一下，直接写死元件的宽高(320, 180)，实际上应该用 <code>getBoundingClientRect</code> 计算是比较好的。</p><p>现在我们就完成整个应用囉！</p><blockquote><p><a href="https://jsfiddle.net/s6323859/ochbtpk5/3/" target="_blank" rel="noopener noreferrer">这里</a>有最后完成的结果。</p></blockquote><h2 id="今日结语"><a href="#今日结语" aria-hidden="true" class="header-anchor">#</a> 今日结语</h2><p>我们简单地用了不到 35 行的代码，完成了一个还算复杂的功能。更重要的是我们还保持了整支程式的可读性，让我们之后维护更加的轻松。</p><p>今天的练习就到这边结束了，不知道读者有没有收获呢？ 如果有任何问题欢迎在下方留言给我！</p><p>如果你喜欢本篇文章请帮我按个 star 。</p></div></div></div><div class="v-card__actions"><div class="flex xs12"><a href="/BLOG/tags/RXJS"><span tabindex="0" class="v-chip capitalize chip-tag v-chip--label v-chip--small"><span class="v-chip__content">RXJS</span></span></a></div></div></article></div><div class="flex text-xs-left xs6"><a href="/BLOG/posts/rxjs12.html" class="post-nav v-btn v-btn--flat v-btn--router"><div class="v-btn__content"><div class="grey--text"><i class="fa mr-1 fa-chevron-left"></i>Prev</div><div class="title mt-1 primary--text hidden-xs-only">12 Observable Operator - scan, buffer</div></div></a></div><div class="flex text-xs-right xs6"><a href="/BLOG/posts/rxjs10.html" class="post-nav v-btn v-btn--flat v-btn--router"><div class="v-btn__content"><div class="grey--text">Next
          <i class="fa ml-1 fa-chevron-right"></i></div><div class="title mt-1 primary--text hidden-xs-only">10 Observable Operator - combineLatest, withLatestFrom, zip</div></div></a></div><div class="flex mt-3 xs12"><div class="v-card" style="height:undefined;"><div class="v-card__title"><span class="headline">Comment</span></div></div></div></div></div><footer class="v-footer blog-footer darken-1 mt-3 theme--dark" style="height:auto;"><div class="primary--text text--lighten-4 text-xs-center py-3 v-card v-card--flat v-card--tile primary" style="height:undefined;"><div class="v-card__text pb-0">博客内容遵循 <a rel="license noopener noreferrer" href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh" target="_blank">知识共享 署名 - 非商业性 - 相同方式共享 4.0 国际协议</a></div><div class="v-card__text pt-0 mt-1"><span>訾绍飞 © 2015 - 2018</span><span><!---->
        Power by
        <a href="https://vuepress.vuejs.org" target="_blank" rel="noopener noreferrer">VuePress</a> Theme
        <a href="https://github.com/ShaofeiZi/BLOG" target="_blank" rel="noopener noreferrer">indigo</a></span></div></div></footer></div></main><button type="button" class="v-btn v-btn--bottom v-btn--floating v-btn--fixed v-btn--right accent" style="display:none;"><div class="v-btn__content"><i class="fa fa-lg fa-chevron-up"></i></div></button></div></div></div>
    <script src="/BLOG/assets/js/23.e624ba97.js" defer></script><script src="/BLOG/assets/js/app.1a725be8.js" defer></script>
  </body>
</html>
